package com.divillysausages.dsair.appupdate 
{
	import air.update.ApplicationUpdater;
	import air.update.events.DownloadErrorEvent;
	import air.update.events.StatusUpdateErrorEvent;
	import air.update.events.StatusUpdateEvent;
	import air.update.events.UpdateEvent;
	import com.divillysausages.dsair.DSAir;
	import com.divillysausages.dsair.windows.Alert;
	import com.divillysausages.dsair.windows.ProgressAlert;
	import flash.desktop.Updater;
	import flash.events.ErrorEvent;
	import flash.events.ProgressEvent;
	import flash.filesystem.File;
	import flash.system.System;
	
	/**
	 * The AppUpdateManager controls the auto-updating of the app
	 * @author Damian Connolly
	 */
	public class AppUpdateManager 
	{
		
		/********************************************************************/
		
		private static var m_instance:AppUpdateManager	= null;	// the only instance of the app update manager
		private static var m_creating:Boolean			= false;// are we creating the app update manager?
		
		/********************************************************************/
		
		/**
		 * The singleton instance for the app update manager
		 */
		public static function get instance():AppUpdateManager
		{
			if ( AppUpdateManager.m_instance == null )
			{
				AppUpdateManager.m_creating = true;
				AppUpdateManager.m_instance = new AppUpdateManager;
				AppUpdateManager.m_creating = false;
			}
			return AppUpdateManager.m_instance;
		}
		
		/********************************************************************/
		
		/**
		 * When checking for an update and there's none available, should we show the
		 * "No update is available" alert? The default is true
		 */
		public var showNoUpdateAvailableAlert:Boolean = true;
		
		/********************************************************************/
		
		private var m_appUpdater:ApplicationUpdater = null; // the app updater object
		private var m_downloadAlert:ProgressAlert	= null; // the alert window for downloading the update
		private var m_showNoUpdateAlert:Boolean		= true; // should we show the 
		
		/********************************************************************/
		
		/**
		 * Is this the first run after a new install?
		 */
		public function get isFirstRun():Boolean { return this.m_appUpdater.isFirstRun; }
		
		/********************************************************************/
		
		/**
		 * Creates the AppUpdateManager. As it's an instance, use the static instance getter instead
		 */
		public function AppUpdateManager() 
		{
			if ( !AppUpdateManager.m_creating )
				throw new Error( "The AppUpdateManager is a singleton. Use the static instance property instead" );
				
			if ( Updater.isSupported )
			{
				// create the app updater
				this.m_appUpdater = new ApplicationUpdater;
				
				// add our event listeners
				this.m_appUpdater.addEventListener( UpdateEvent.INITIALIZED, this._onInitialised );
				this.m_appUpdater.addEventListener( ErrorEvent.ERROR, this._onError );
				this.m_appUpdater.addEventListener( StatusUpdateEvent.UPDATE_STATUS, this._onStatusUpdate );
				this.m_appUpdater.addEventListener( StatusUpdateErrorEvent.UPDATE_ERROR, this._onStatusUpdateError );
				this.m_appUpdater.addEventListener( ProgressEvent.PROGRESS, this._onDownloadProgress );
				this.m_appUpdater.addEventListener( UpdateEvent.DOWNLOAD_COMPLETE, this._onDownloadComplete );
				this.m_appUpdater.addEventListener( DownloadErrorEvent.DOWNLOAD_ERROR, this._onDownloadError );
			}
			else
				DSAir.error( this, "The app updater is not supported in this config" );
		}
		
		/**
		 * Generates the XML file to use for our update
		 * @param appURL The URL where we can download the app (including /APP_NAME.air)
		 * @param notes Any diff notes that we want to add to display to the user
		 * @param version The version of the app. It should be in the form (0..999).(0..999).(0..999)
		 * @param air25 Set to true if we're compiling on >= AIR2.5
		 */
		public function generateUpdateXML( appURL:String, notes:String = "", version:String = "1.0.0", air25:Boolean = false ):void
		{
			// create our xml
			var x:XML = <update>
					<url>{appURL}</url>
					<description>{this._generateCDATA( notes )}</description>
				</update>
				
			// add the namespace and version
			// we need to add the namespace as an attribute, as the addNamespace() function
			// won't let you add a namespace that's not named (i.e. xmlns=[address], instead of
			// xmlns:someName=[address]
			if ( air25 )
			{
				x.@xmlns = "http://ns.adobe.com/air/framework/update/description/2.5";
				x.appendChild( <versionNumber>{version}</versionNumber> );
			}
			else
			{
				x.@xmlns = "http://ns.adobe.com/air/framework/update/description/1.0";
				x.appendChild( <version>{version}</version> );
			}
			
			// create the full xml (add the processing instruction using a string)
			var s:String = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + x.toXMLString();
			
			// kill the xml
			System.disposeXML( x );
			
			// save it
			var file:File = new File();
			file.save( s, "update.xml" );
		}
		
		/**
		 * Checks to see if we have an update available
		 * @param xmlURL The URL of the xml that has our version info
		 */
		public function checkForUpdate( xmlURL:String ):void
		{
			// no app updater means it's not supported
			if ( this.m_appUpdater == null )
				return;
				
			// set the url
			this.m_appUpdater.updateURL = xmlURL;
			
			// create our app updater if needed
			if ( this.m_appUpdater.currentState != "READY" )
				this.m_appUpdater.initialize();
			else
				this._checkForUpdates();
		}
		
		/********************************************************************/
		
		// generates cdata for an xml
		private function _generateCDATA( text:String ):XML
		{
			return new XML("<![CDATA[" + text + "]]>");
		}

		// checks to see if there's a newer version of the app available
		private function _checkForUpdates():void
		{
			DSAir.log( this, "Checking for updates at url: " + this.m_appUpdater.updateURL );
			this.m_appUpdater.checkNow();
		}
		
		// the application update is initialised and is in a state to check for updates
		private function _onInitialised( e:UpdateEvent ):void
		{
			this._checkForUpdates();
		}
		
		// an error has occurred - show it
		private function _onError( e:ErrorEvent ):void
		{
			DSAir.error( this, "An error has occurred while updating: " + e.errorID + ": " + e.text );
			this._showError( e.errorID, e.text );
		}
		
		// called after the update descriptor was downloaded and interpreted successfully
		private function _onStatusUpdate( e:StatusUpdateEvent ):void
		{
			// prevent the default (start downloading the new version)
			e.preventDefault();
			
			// create our window
			var alert:Alert = null;
			
			// show that an update is available
			if ( e.available ) 
			{
				alert = new Alert( "An update is available", "A new version (" + e.version + ") is available" + "\n\n<b>Details:</b>\n" + e.details[0][1] );
				alert.addButton( "Close", this._onClose );
				alert.addButton( "Update", this._startDownload );
			} 
			else if( this.showNoUpdateAvailableAlert ) // only show the alert if wanted
			{
				alert = new Alert( "No update is available", "Your version is up to date" );
				alert.addButton( "Close", this._onClose );
			}
			
			// show the alert
			if( alert != null )
				alert.show();
		}
		
		// called when the updater couldn't download or interpret the descriptor file
		private function _onStatusUpdateError( e:StatusUpdateErrorEvent ):void
		{
			trace( "update xml: " + this.m_appUpdater.updateDescriptor );
			DSAir.error( this, "An error has occurred while downloading or interpreting the descriptor file: " + e.errorID + " (" + e.subErrorID + "): " + e.text );
			this._showError( e.errorID, "(" + e.subErrorID + ") " + e.text );
		}
		
		// called when we close the alert window saying there's an update available,
		// the window that says we're up to date, or the download progress window
		// (this resets everything)
		private function _onClose():void
		{
			this.m_appUpdater.cancelUpdate();
		}
		
		// called when we click on a button or whatever to start the download 
		// of the update
		private function _startDownload():void
		{
			this.m_appUpdater.downloadUpdate();
			
			// create our download progress window
			this.m_downloadAlert 			= new ProgressAlert( "Downloading update...", "Downloading..." );
			this.m_downloadAlert.progress	= 0.0;
			this.m_downloadAlert.addButton( "Cancel", this._onClose );
			this.m_downloadAlert.show();
		}
		
		// called while the new version is downloading
		private function _onDownloadProgress( e:ProgressEvent ):void
		{
			if ( this.m_downloadAlert != null )
				this.m_downloadAlert.progress = e.bytesLoaded / e.bytesTotal;
		}
		
		// called when the new version has downloaded - just close the window; the downloaded
		// version gets automatically installed and the app will restart
		private function _onDownloadComplete( e:UpdateEvent ):void
		{
			this._killDownloadAlert();
		}
		
		// called if there's an error while connecting or downloading the update file (or
		// invalid HTTP statuses, like 404)
		private function _onDownloadError( e:DownloadErrorEvent ):void
		{
			DSAir.error( this, "An error has occurred while downloading the new version: " + e.errorID + " (" + e.subErrorID + "): " + e.text );
			this._showError( e.errorID, "(" + e.subErrorID + ") " + e.text );
			
			// kill the download alert
			this._killDownloadAlert();
		}
		
		// shows an error
		private function _showError( errorID:int, errorText:String ):void
		{
			var alert:Alert = new Alert( "Error", "Error ID: " + errorID + ", msg: " + errorText );
			alert.addButton( "Close" );
			alert.show();
		}
		
		// destroys the download alert window if it exists
		private function _killDownloadAlert():void
		{
			if ( this.m_downloadAlert != null )
			{
				this.m_downloadAlert.destroy();
				this.m_downloadAlert = null;
			}
		}
		
	}

}